package com.seventhdawn.gwt.rpc.scripttag.server;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import com.seventhdawn.gwt.rcx.server.AbstractRPCFilter;
import com.seventhdawn.gwt.rpc.scripttag.client.ScriptTagServiceProxy;

/**
 * Servlet {@link javax.servlet.Filter filter} that handles cross-site RPC
 * requests via JSONP (HTTP GET).
 * <p>
 * The filter extracts the RPC request payload and wraps the original request as
 * HTTP POST suitable for standard RPC processing. Response is wrapped as well,
 * sending the RPC response payload as JSONP back to the client.
 * <p>
 * <b>Important:</b> This filter needs to be applied <em>before
 * your server-side RPC service implementation</em> (e.g. a
 * {@link com.google.gwt.user.server.rpc.RemoteServiceServlet
 * RemoteServiceServlet} subclass) takes over the RPC request processing.
 * 
 * @author vojtech.szocs
 * @author Artem Kozarezov
 */
public class ScriptTagFilter extends AbstractRPCFilter {

    private static final String CHARSET_UTF8 = "UTF-8";
    private static final String METHOD_POST = "POST";
    private static final String RPC_CONTENT_TYPE = "text/x-gwt-rpc";
    private static final String JSONP_CONTENT_TYPE = "application/javascript";

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        doFilterInit(httpRequest.getContextPath());

        HttpServletRequest filteredRequest = processRequest(httpRequest);
        HttpServletResponse filteredResponse = processResponse(httpRequest, httpResponse);

        chain.doFilter(filteredRequest, filteredResponse);
    }

    /**
     * Extracts the RPC request payload and wraps the original request as HTTP
     * POST suitable for standard RPC processing.
     */
    protected HttpServletRequest processRequest(HttpServletRequest request) throws IOException, ServletException {
        String rpcPayloadEncoded = request.getParameter(ScriptTagServiceProxy.URL_PARAM_REQUEST_PAYLOAD);
        if (rpcPayloadEncoded == null)
            throw new ServletException("Missing RPC request payload parameter");

        final byte[] rpcPayload = URLDecoder.decode(rpcPayloadEncoded, CHARSET_UTF8).getBytes();
        final ByteArrayInputStream rpcInputStream = new ByteArrayInputStream(rpcPayload);

        return new HttpServletRequestWrapper(request) {

            @Override
            public String getMethod() {
                return METHOD_POST;
            }

            @Override
            public int getContentLength() {
                return rpcPayload.length;
            }

            @Override
            public String getContentType() {
                return RPC_CONTENT_TYPE;
            }

            @Override
            public String getCharacterEncoding() {
                return CHARSET_UTF8;
            }

            @Override
            public ServletInputStream getInputStream() throws IOException {
                return new ServletInputStream() {

                    @Override
                    public int available() throws IOException {
                        return rpcInputStream.available();
                    }

                    @Override
                    public int read() throws IOException {
                        return rpcInputStream.read();
                    }

                    @Override
                    public int read(byte[] b, int off, int len) throws IOException {
                        return rpcInputStream.read(b, off, len);
                    }

                };
            }

        };
    }

    /**
     * Wraps the RPC response payload as JSONP.
     */
    protected HttpServletResponse processResponse(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        final String callback = request.getParameter(ScriptTagServiceProxy.URL_PARAM_CALLBACK);
        if (callback == null)
            throw new ServletException("Missing JSONP callback function parameter");

        final ServletOutputStream responseStream = response.getOutputStream();

        return new HttpServletResponseWrapper(response) {

            private int rpcPayloadLength;

            @Override
            public String getContentType() {
                return JSONP_CONTENT_TYPE;
            }

            @Override
            public String getCharacterEncoding() {
                return CHARSET_UTF8;
            }

            @Override
            public void setContentLength(int len) {
                this.rpcPayloadLength = len;
            }

            @Override
            public ServletOutputStream getOutputStream() throws IOException {
                return new ServletOutputStream() {

                    private ByteArrayOutputStream rpcPayloadBuffer;

                    @Override
                    public void write(int b) throws IOException {
                        ensurePayloadBuffer();
                        rpcPayloadBuffer.write(b);

                        if (rpcPayloadBuffer.size() == rpcPayloadLength)
                            writeResponse(rpcPayloadBuffer.toString(CHARSET_UTF8));
                    }

                    private void ensurePayloadBuffer() {
                        if (rpcPayloadBuffer == null)
                            rpcPayloadBuffer = new ByteArrayOutputStream(rpcPayloadLength);
                    }

                    private void writeResponse(String rpcPayload) throws IOException {
                        byte[] responseBytes = getResponseAsString(rpcPayload).getBytes(CHARSET_UTF8);
                        responseStream.write(responseBytes);
                    }

                    private String getResponseAsString(String rpcPayload) throws IOException {
                        String rpcPayloadEncoded = URLEncoder.encode(rpcPayload, CHARSET_UTF8);

                        // prepare JSONP response
                        StringBuilder jsonp = new StringBuilder();
                        jsonp.append(callback).append("({");
                        jsonp.append("\"").append(ScriptTagServiceProxy.JSON_PARAM_RESPONSE_PAYLOAD).append("\":");
                        jsonp.append("\"").append(rpcPayloadEncoded).append("\"");
                        jsonp.append("})");

                        return jsonp.toString();
                    }

                };
            }

        };
    }

}
